/*
 * MrSudokuApp.java
 */
package mrsudoku;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jdesktop.application.Application;
import org.jdesktop.application.SingleFrameApplication;

/**
 * The main class of the application. DIRK
 */
public class MrSudokuApp extends SingleFrameApplication
{

    // TODO getter und setter für die arrays benutzen
    private int[][] arr;               //Beinhaltet die Zahlen, die in der GUI angezeigt werden.
    private int[][] arrSolved;         //Beinhaltet alle Zahlen. Das gelöste Sudoku.
    private boolean[][] arrEditable;   //Beinhaltet welche Felder in der GUI editierbar und enabled sind. True für editierbar.
    final public int m = 9;
    final public int n = 9;
    int countLoopRun;     //Zählt wie oft die Methode solve() bis zur vollständigen Lösung des Sudokus aufgerufen wurde.
    private File activeFile; // speichert das im moment vom user genutzte dokument
    private boolean gameMode = false; // false == edit , true == game

    MrSudokuApp()
    {
        arr = new int[m][n];
        arrSolved = new int[m][n];
        arrEditable = new boolean[m][n];
        for (int i = 0; i < arrEditable.length; i++)
            for (int j = 0; j < arrEditable.length; j++)
                arrEditable[i][j] = true;
        countLoopRun = 0;

        this.setActiveFile(new File("temp.sdf"));



    }
    
    /**
     *
     * @return
     */
    public boolean[][] getArrEditable()
    {
        return arrEditable;
    }

    /**
     *
     * @return
     */
    public boolean getArrEditable(int i, int j)
    {
        return arrEditable[i][j];
    }

    /**
     *
     * @param arr
     */
    public void setArrEditable(boolean state, int i, int j)
    {
        arrEditable[i][j] = state;
    }

    /**
     * 
     * @return 
     */
    public int[][] getArrSolved()
    {
        return arrSolved;
    }

    /**
     * 
     * @param arr 
     */
    public void setArrSolved(int[][] solved)
    {
        arrSolved = solved;
    }

    /**
     * 
     * @return 
     */
    public File getActiveFile()
    {
        return activeFile;
    }

    /**
     * 
     * @param activeFile 
     */
    public void setActiveFile(File activeFile)
    {
        this.activeFile = activeFile;
    }

    /**
     * 
     * @return 
     */
    public int[][] getArr()
    {
        return arr;
    }

    /**
     * 
     * @param arr 
     */
    public void setArr(int[][] newArr)
    {
        this.arr = newArr;
    }

    /**
     *
     * @return
     */
    public boolean getGameMode()
    {
        return gameMode;
    }

    /**
     *
     * @param arr
     */
    public void setGameMode(boolean state)
    {
        this.gameMode = state;
    }

    /**
     * At startup create and show the main frame of the application.
     */
    @Override
    protected void startup()
    {
        show(new MrSudokuView(this));
    }

    /**
     * This method is to initialize the specified window by injecting resources.
     * Windows shown in our application come fully initialized from the GUI
     * builder, so this additional configuration is not needed.
     */
    @Override
    protected void configureWindow(java.awt.Window root)
    {
    }

    /**
     * A convenient static getter for the application instance.
     * @return the instance of MrSudokuApp
     */
    public static MrSudokuApp getApplication()
    {
        return Application.getInstance(MrSudokuApp.class);
    }

    /**
     * Main method launching the application.
     */
    public static void main(String[] args)
    {
        launch(MrSudokuApp.class, args);
    }

    /**
     * author: MA
     * Prüfung der Sudokuregel, dass eine Zahl("eingabeZahl") nicht mehrmals in einer Reihe("row"), Spalte("col") und 3X3 Kasten vorkommen darf.
     * Kommt die Zahl ("eingabeZahl") bereits in einer der zuvor genannten Positionen vor, wird "false" zurückgeliefert.
     * Ansonsten wird "true" zurückgeliefert.
     * @param eingabeZahl
     * @param row
     * @param col
     * @return 
     */
    private boolean areSudokuRulesComplied(int eingabeZahl, int row, int col) //Prüfung der Sudokuregel, dass eine Zahl("eingabeZahl") nicht mehrmals in einer Reihe("row"), Spalte("col") und 3X3 Kasten vorkommen darf.
    {
        //1. Pruefung
        for (int i = 0; i < m; i++) //Prüefung ob eingabeZahl schon in der angegebenen Spalte vorhanden ist.
        {
            if (this.arrSolved[i][col] == eingabeZahl)
            {
                return false;
            }
        }
        //2. Pruefung
        for (int j = 0; j < n; j++) //Prüefung ob eingabeZahl schon in der angegebenen Spalte vorhanden ist.
        {
            if (this.arrSolved[row][j] == eingabeZahl)
            {
                return false;
            }
        }
        //Vorbereitungen für die 3. Pruefung
        int rowLoopAdjustment = 0;
        int colLoopAdjustment = 0;

        rowLoopAdjustment = row % 3;
        colLoopAdjustment = col % 3;
        //3.Pruefung
        for (int i = row - rowLoopAdjustment; i < row - rowLoopAdjustment + 3; i++) //Prüefung ob eingabeZahl schon im angegebenen 3X3 Kasten vorhanden ist.
        {
            for (int j = col - colLoopAdjustment; j < col - colLoopAdjustment + 3; j++)
            {
                if (this.arrSolved[i][j] == eingabeZahl)
                {
                    return false;
                }
            }
        }

        return true;
    }

    /**
     * author: MA
     * Prüfung der Sudokuregel, dass eine Zahl("eingabeZahl") mindestens einmal in einer Reihe("row"), Spalte("col") und 3X3 Kasten vorkommen muss.
     * Liefert "true", wenn die übergebene Zahl("eingabeZahl") in der entsprechenden Zeile oder Spalte oder 3x3 Kasten durch Ausschlussverfahren als die einzig mögliche Zahl ermittelt wurde.
     * Dies ist der Fall wenn die Zahl("eingabeZahl") für keines der Felder der entsprechenden Zeile oder Spalte oder 3x3 Kasten, zu den möglichen Werten gehört.
     * Ansonsten wird "false" zurückgeliefert.
     * @param eingabeZahl
     * @param row
     * @param col
     * @return 
     */
    private boolean isNumberTheOnlyPossibleOne(int eingabeZahl, int row, int col) //Prüfung der Sudokuregel, dass eine Zahl("eingabeZahl") mindestens einmal in einer Reihe("row"), Spalte("col") und 3X3 Kasten vorkommen muss.
    {
        boolean isNumberTheOnlyPossibleInRow = true;
        boolean isNumberTheOnlyPossibleInCol = true;
        boolean isNumberTheOnlyPossibleIn3X3 = true;

        //1. Pruefung. Ist eingabeZahl in einem Feld der übergebenen Reihe (Variable row) eine mögliche Lösung?
        for (int i = 0; i < m; i++)
        {
            if (this.arrSolved[i][col] == 0 && i != row)
            {
                if (areSudokuRulesComplied(eingabeZahl, i, col)) //liefert areSudokuRulesComplied ein true, gehört "eingabeZahl" zu den möglichen Lösungen für die aktuelle Array-Position.
                {
                    isNumberTheOnlyPossibleInRow = false;                         //Deshalb konnte "eingabeZahl" in der ursprünglichen Reihe(Variable row) nicht als eindeutig richtige Lösung identifiziert werden.
                    break;
                }
            }
        }
        if (isNumberTheOnlyPossibleInRow) //ist isEingabeZahlTheWrongNumber false, konnte eingabeZahl also als eindeutig richtiger Lösung identifiziert werden.
        {
            return true;
        }

        //2. Pruefung. Ist eingabeZahl in einem Feld der übergebenen Spalte (Variable col) eine mögliche Lösung?
        for (int j = 0; j < n; j++)
        {
            if (this.arrSolved[row][j] == 0 && j != col)
            {
                if (areSudokuRulesComplied(eingabeZahl, row, j)) //liefert areSudokuRulesComplied ein true, gehört "eingabeZahl" zu den möglichen Lösungen für die aktuelle Array-Position.
                {
                    isNumberTheOnlyPossibleInCol = false;                         //Deshalb konnte "eingabeZahl" in der ursprünglichen Spalte(Variable col) nicht als eindeutig richtige Lösung identifiziert werden.
                    break;
                }
            }
        }
        if (isNumberTheOnlyPossibleInCol) //ist isEingabeZahlTheWrongNumber false, konnte eingabeZahl also als eindeutig richtiger Lösung identifiziert werden.
        {
            return true;
        }

        //Vorbereitung für die 3. Pruefung
        int rowLoopAdjustment = 0;
        int colLoopAdjustment = 0;

        rowLoopAdjustment = row % 3;
        colLoopAdjustment = col % 3;
        //3. Pruefung. Ist eingabeZahl in einem Feld des 3X3 Kasten (durch Variable col und row bestimmt) eine mögliche Lösung?
        for (int i = row - rowLoopAdjustment; i < row - rowLoopAdjustment + 3; i++) //Prüefung ob eingabeZahl schon im angegebenen 3X3 Kasten vorhanden ist.
        {
            for (int j = col - colLoopAdjustment; j < col - colLoopAdjustment + 3; j++)
            {
                if (this.arrSolved[i][j] == 0)
                {
                    if (!(j == col & i == row))
                    {
                        if (areSudokuRulesComplied(eingabeZahl, i, j))
                        {
                            isNumberTheOnlyPossibleIn3X3 = false;                         //Deshalb konnte "eingabeZahl" in der ursprünglichen Spalte(Variable col) nicht als eindeutig richtige Lösung identifiziert werden.
                            break;
                        }
                    }
                }
            }
        }
        if (isNumberTheOnlyPossibleIn3X3) //ist isEingabeZahlTheWrongNumber false, konnte eingabeZahl also als eindeutig richtiger Lösung identifiziert werden.
        {
            return true;
        }

        return false;
    }

    /**
     * author: MA
     * Initialisiert die Lösung des Sudokus. Kopiert die Werte aus arr[][] in arrSolved[][].
     */
    public void solveInitialize() throws SolveException
    {
        for (int i = 0; i < this.m; i++)
        {
            for (int j = 0; j < this.n; j++)
            {
                arrSolved[i][j] = arr[i][j];
            }
        }
        solve();
    }

    /**
     * author: MA
     * Hauptalgorithmus zum lösen des Sudokus. Ruft sich durch Rekursion solange auf, bis das Sudoku vollständig gelöst ist oder bis festgestellt wurde, dass das Sudoku nicht eindeutig lösbar ist.
     * Liefert "true", wenn das Sudoku eindeutig lösbar ist.
     * Ansonsten wird "false" zurückgeliefert.
     */
    public void solve() throws SolveException
    {
        boolean solveAgain = false;             //Gibt an ob die Methode solve() sich nochmal selbst aufrufen soll (Rekursion).

        for (int i = 0; i < this.m; i++) //Schleife fürs Durchlaufen der Zeilen des Arrays.
        {
            for (int j = 0; j < this.n; j++) //Schleife fürs Durchlaufen der Spalten des Arrays.
            {
                if (this.arrSolved[i][j] != 0) //Wenn in der aktuelle Array-Position schon ein Wert steht, wird der aktuelle Schleifendurchlauf abgebrochen.
                {
                    continue;
                } else //enthält die aktuelle Array-Position noch keinen Wert, werden nun die möglichen Werte ermittelt.
                {
                    int[] arrMoeglicheWerte = new int[9];
                    int wert = 0;                     //Der Wert der bei einer eindeutigen Lösung in die entsprechende Array-Position geschrieben wird (also wenn anzMoeglicheWerte = 1).
                    int anzMoeglicheWerte = 0;        //Gibt die Anzahl der möglichen Werte für die aktuelle Array-Position an.

                    for (int z = 1; z <= 9; z++) //Die Schleife durchläuft die zulässigen Werte (Werte von 1-9).
                    {
                        if (areSudokuRulesComplied(z, i, j)) //Sudoku.areSudokuRulesComplied liefert true, wenn z nicht in der aktuellen Zeile, Spalte und 3X3 Kasten steht.
                        {
                            wert = z;                   //ist z also ein möglicher Kandidat für die aktuelle Array-Position wird er in Variable wert geschrieben.
                            arrMoeglicheWerte[anzMoeglicheWerte] = wert;
                            anzMoeglicheWerte++;
                        }
                    }
                    if (anzMoeglicheWerte == 1) //Wenn es nur einen möglichen Kandidaten für die aktuelle Array-Position gibt, wird dieser in die aktuelle Array-Position geschrieben.
                    {
                        this.arrSolved[i][j] = wert;
                    } else //Ansonsten muss die Methode solve() im späteren Verlauf nochmal aufgerufen werden, da es noch Array-Positionen gibt, für die noch kein eindeutiger Wert ermittelt werden konnte.
                    {
                        for (int k = 0; k < anzMoeglicheWerte; k++)
                        {
                            if (isNumberTheOnlyPossibleOne(arrMoeglicheWerte[k], i, j))
                            {
                                wert = arrMoeglicheWerte[k];
                                this.arrSolved[i][j] = wert;
                                break;
                            }
                        }
                        if (this.arrSolved[i][j] == 0) //Wenn immer noch nicht der richtige Wert für die aktuelle Array-Position  gefunden wurde, soll sich die Methode "solve()" zu einem späteren Zeitpunkt nochmal aufrufen.
                        {
                            solveAgain = true;
                        }
                    }
                }
            }
        }

        countLoopRun++;

        if (countLoopRun >= 65)
        {                           //ein eindeutig lösbares Sudoku besitzt mindestens 17 Werte. Bei jedem durchlauf der methode wird mindestens eine weitere Zahl ermittelt, wenn das sudoku eindeutig lösbar ist..
            arrSolved = new int[9][9];
            countLoopRun = 0;
            
            throw new SolveException();

        }

        if (solveAgain) //Wenn noch nicht für alle Array-Positionen ein eindeutiger Wert ermittelt werden konnte, ruft sich die Methode wieder selber auf.
        {
            solve();
        }

        System.out.println(countLoopRun);
        countLoopRun = 0;
    }

    /**
     * author: MA
     * Liefert "true", wenn vom Benutzer mindestens eine falsche Zahl in der GUI eingetragen wurde.
     * Ansonsten wird "false" zurückgeliefert.
     */
    public boolean pruefen()
    {
        for (int i = 0; i < this.m; i++)
        {
            for (int j = 0; j < this.n; j++)
            {
                if (arr[i][j] != 0)
                {
                    if (arr[i][j] != arrSolved[i][j])
                    {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    /**
     * author: MA
     * Liefert ein boolean-Array zurück.
     * Weicht eine Zahl in der GUI von der richtigen durch den Loesungsalgoruthmus ermittelten Zahl ab, wird das entsprechende Element im boolean-Array auf "false" gesetzt.
     */
    public boolean[][] fehlerAnzeigen()
    {
        boolean[][] temp = new boolean[m][n];
        for (int i = 0; i < this.m; i++)
        {
            for (int j = 0; j < this.n; j++)
            {
                temp[i][j] = true;
            }
        }

        for (int i = 0; i < this.m; i++)
        {
            for (int j = 0; j < this.n; j++)
            {
                if (arr[i][j] != 0)
                {
                    if (arr[i][j] != arrSolved[i][j])
                    {
                        temp[i][j] = false;
                    }
                }
            }
        }

        return temp;
    }

    /**
     * author: MA
     * Wird aufgerufen, wenn kein Feld in der GUI angewählt wurde, oder ein Feld angewählt wurde,
     * in dem bereits eine Zahl steht.
     * Liefert die richtige Zahl für ein zufälliges Feld
     */
    public int[] loesungshinweis()
    {
        int[] temp = new int[3];

        boolean notEveryFieldFilled = false;
        Random random = new Random();

        int a = random.nextInt(m);
        int b = random.nextInt(n);

        for (int i = 0; i < arr.length; i++)
        {
            for (int j = 0; j < arr.length; j++)
            {
                if (arr[i][j] == 0)
                {
                    notEveryFieldFilled = true;
                }
            }
        }

        if (notEveryFieldFilled)
        {
            while (arr[a][b] != 0)
            {
                a = random.nextInt(m);
                b = random.nextInt(n);
            }
            arr[a][b] = arrSolved[a][b];
        }

        temp[0] = a;
        temp[1] = b;
        temp[2] = arrSolved[a][b];

        return temp;
    }

    /**
     * 
     * @param i
     * @param j
     * @return 
     */
    public int getArrayElement(int i, int j)
    {
        return arr[i][j];
    }

    /**
     * 
     * @param i
     * @param j
     * @param value 
     */
    public void setArrayElement(int i, int j, int value)
    {
        if (value < 10 && value > -1) // between 0 and 9
        {
            arr[i][j] = value;

        }
    }

    /**
     * 
     * @param file
     * @return 
     */
    public boolean open(File file)
    {

        if (!file.isFile())
        {
            throw new IllegalArgumentException("File object submitted is not a file.");
            //todo log
        }

        FileReader reader;
        BufferedReader buff;
        String s;


        try
        {
            reader = new FileReader(file);
            buff = new BufferedReader(reader);


            if ((s = buff.readLine()) != null)
            {
                System.out.println("reading :" + s);

            }
            if (s != null)
            {
                //todo test if 81 digits

                String[] digits = s.split(" ");
                System.out.println(digits.length);

                int[][] numbers = new int[9][9];

                int pos = 0;
                for (int i = 0; i < 9; ++i)
                {

                    for (int j = 0; j < 9; ++j)
                    {
                        try
                        {

                            numbers[i][j] = Integer.parseInt(digits[pos]);
                            //System.out.println(pos + "  :  " +numbers[i][j]);

                        } catch (NumberFormatException nfe)
                        {
                            //System.out.println("oops");
                            Logger.getLogger(MrSudokuApp.class.getName()).log(Level.WARNING, null, nfe);
                            numbers[i][j] = 0;
                        }

                        pos++;
                    }
                }

                setArr(numbers);


                int[][] numbersSolved = new int[9][9];

                for (int i = 0; i < 9; ++i)
                {
                    for (int j = 0; j < 9; ++j)
                    {
                        try
                        {
                            numbersSolved[i][j] = Integer.parseInt(digits[pos]);

                        } catch (NumberFormatException nfe)
                        {
                            Logger.getLogger(MrSudokuApp.class.getName()).log(Level.WARNING, null, nfe);
                            numbersSolved[i][j] = 0;
                        }

                        pos++;
                    }
                }

                setArrSolved(numbersSolved);

            } else
            {
                // file is empty so set empty array
                //todo log
                System.out.println("empty file");
                setArr(new int[9][9]);
            }
        } catch (FileNotFoundException ex)
        {
            Logger.getLogger(MrSudokuApp.class.getName()).log(Level.SEVERE, null, ex);
            return false;
        } catch (IOException ioe)
        {
            Logger.getLogger(MrSudokuApp.class.getName()).log(Level.SEVERE, null, ioe);
            return false;
        }


        setActiveFile(file);


        return true;
    }

    /**
     * 
     * @param file
     * @return 
     */
    public boolean save(File file)
    {

        if (!file.getName().endsWith(".sdf"))
        {
            //System.out.println(file.getAbsolutePath());
            file = new File(file.getAbsolutePath() + ".sdf");
            //System.out.println(file.getAbsolutePath());
        }

        if (!file.isFile())
        {
            try
            {

                file.createNewFile();
            } catch (IOException ex)
            {
                Logger.getLogger(MrSudokuApp.class.getName()).log(Level.SEVERE, null, ex);
                return false;
            }
        } else
        {
            //delete contents
            file.delete();

            try
            {
                file.createNewFile();
            } catch (IOException ex)
            {
                Logger.getLogger(MrSudokuApp.class.getName()).log(Level.SEVERE, null, ex);
            }
        }





        FileWriter writer = null;

        try
        {
            writer = new FileWriter(file);
            // todo arr in file schreiben

            int[][] temp = getArr();

            for (int i = 0; i < 9; ++i)
            {
                for (int j = 0; j < 9; ++j)
                {
                    writer.write(temp[i][j] + " ");

                }
            }

            temp = getArrSolved();

            for (int i = 0; i < 9; ++i)
            {
                for (int j = 0; j < 9; ++j)
                {
                    writer.write(temp[i][j] + " ");

                }
            }

            writer.flush();


        } catch (IOException ioe)
        {
            //todo log and update status
        } finally
        {
            try
            {
                writer.close();
            } catch (IOException ex)
            {
                Logger.getLogger(MrSudokuApp.class.getName()).log(Level.SEVERE, null, ex);
            }
        }


        setActiveFile(file);

        return true;

    }

    public static void printArr(int[][] p)
    {


        for (int i = 0; i < 9; ++i)
        {

            for (int j = 0; j < 9; ++j)
            {
                System.out.print(p[i][j] + " , ");
            }
            System.out.println();
        }
    }
}
